/*
 *  Copright: 2008 Philipp Leitner, Distributed Systems Group,
 *            TU Wien, leitner@infosys.tuwien.ac.at
 *
 *  This file is part of DAIOS.
 *
 *  DAIOS is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser General Public License as
 *  published by the Free Software Foundation.
 * 
 *  DAIOS is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with DAIOS.  If not, see
 *  <http://www.gnu.org/licenses/>.
 * 
 */

package at.ac.tuwien.infosys.dsg.daiosPlugins.nativeInvoker;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.logging.Logger;

import javax.wsdl.WSDLException;
import javax.xml.namespace.QName;

import org.apache.axiom.om.OMElement;

import at.ac.tuwien.infosys.dsg.daios.exceptions.AmbigiousException;
import at.ac.tuwien.infosys.dsg.daios.exceptions.InvocationException;
import at.ac.tuwien.infosys.dsg.daios.framework.DaiosInterceptor;
import at.ac.tuwien.infosys.dsg.daios.framework.PollObject;
import at.ac.tuwien.infosys.dsg.daios.framework.interfaces.IConstants;
import at.ac.tuwien.infosys.dsg.daios.framework.interfaces.IDaiosCallback;
import at.ac.tuwien.infosys.dsg.daios.framework.interfaces.IServiceFrontendImplementor;
import at.ac.tuwien.infosys.dsg.daios.framework.structs.DaiosInputMessage;
import at.ac.tuwien.infosys.dsg.daios.framework.structs.DaiosOutputMessage;
import at.ac.tuwien.infosys.dsg.daios.wsdl.impl.WSDL;
import at.ac.tuwien.infosys.dsg.daios.wsdl.interfaces.IWSDL;
import at.ac.tuwien.infosys.dsg.daios.wsdl.interfaces.IWSDLMessage;
import at.ac.tuwien.infosys.dsg.daios.wsdl.interfaces.IWSDLOperation;
import at.ac.tuwien.infosys.dsg.daios.wsdl.matcher.WSDLStructuralMatcher;
import at.ac.tuwien.infosys.dsg.daiosPlugins.axis2.Axis2RESTInvoker;
import at.ac.tuwien.infosys.dsg.daiosPlugins.axis2.axiom.OMElementConverter;
import at.ac.tuwien.infosys.dsg.daiosPlugins.nativeInvoker.backend.SOAPBackend;
import at.ac.tuwien.infosys.dsg.daiosPlugins.nativeInvoker.backend.SOAPResponse;
import at.ac.tuwien.infosys.dsg.daiosPlugins.nativeInvoker.converter.SOAPConverter;

/**
 * The service invoker used to call a Web Service using the
 * native (built-in) Web Service client libraries.
 * 
 * @author philipp
 *
 */
public class NativeServiceInvoker implements IServiceFrontendImplementor {
	
	private DaiosInterceptor interceptor = null;
	private Logger log = null;
	private ThreadFactory threadFactory = null;
	private IWSDL wsdl = null;
	private String endpoint = null;
	private IWSDLMessage fixedMessage = null;
	private IWSDLOperation fixedOperation = null;
	private WSDLStructuralMatcher matcher = null;
	private IWSDLMessage outputMsgToUse = null;
	private Hashtable<QName, String> headers = null;
	private Hashtable<String, String> httpHeaders = null;
	private Hashtable<QName, String> responseHeaders = null;
	private Hashtable<String, String> responseHttpHeaders = null;
	
	/**
	 * Construct a new invoker that calls the service
	 * that is defined by the WSDL definition at a certain
	 * URL.
	 * 
	 * @param url The location of the service definition.
	 * 
	 * @throws WSDLException
	 * @throws AmbigiousException 
	 */
	NativeServiceInvoker(URL url, DaiosInterceptor interceptor)
		throws WSDLException, AmbigiousException {
		
		this.interceptor = interceptor;
		
		log = Logger.getLogger(IConstants.LOG_NAME);
		
		// we are using the new Java 5 thread factory
		threadFactory = Executors.defaultThreadFactory();
		
		// parse the WSDL file
		if(interceptor != null)
			interceptor.beginWSDLProcessing(url);
		this.wsdl = new WSDL(url);
		if(interceptor != null) {
			this.wsdl.setInterceptor(interceptor);
			interceptor.finishedWSDLProcessing(url, this.wsdl);
		}
			
		headers = new Hashtable<QName, String>();
		httpHeaders = new Hashtable<String, String>();
		
		// construct a new Matcher
		matcher = new
			WSDLStructuralMatcher(wsdl);
		
		// we also kickoff type parsing now
		this.wsdl.getTypes();
		
	}
	
	/**
	 * Do a synchronous, blocking, request-response style
	 * call. The client will be blocked until the return
	 * message is delivered.
	 * 
	 * @param message The Daios style input message.
	 * @return The Daios style output message.
	 * 
	 * @throws AmbigiousException
	 * @throws InvocationException
	 */
	public DaiosOutputMessage requestResponse(DaiosInputMessage message)
		throws AmbigiousException, InvocationException {
		
		if(interceptor != null)
			interceptor.beginInvocation("SOAP", "requestResponse",
				message, this);
		
		DaiosOutputMessage output = requestResponseInvocation(message);
		
		if(interceptor != null)
			interceptor.finishedInvocation("SOAP", "requestResponse",
				message, this, output);		
		
		return output;
		
	}

	/**
	 * Do a synchronous, blocking, request-response style
	 * call. The client will be blocked until the return
	 * message is delivered.
	 * 
	 * @param message The Daios style input message.
	 * @return The Daios style output message.
	 * 
	 * @throws AmbigiousException
	 * @throws InvocationException
	 */	
	private DaiosOutputMessage requestResponseInvocation(DaiosInputMessage message)
		throws AmbigiousException, InvocationException {
		
		// prepare our Daios invocation
		OMElement body = prepareNativeInvocation(message);
		
		// use our native SOAP backend to transfer the message to
		// the endpoint
		SOAPBackend backend = null;
		try {
			backend = new SOAPBackend(new URL(endpoint), log, headers, httpHeaders);
		} catch (MalformedURLException e) {
			log.severe("Malformed URL detected.");
			e.printStackTrace();
			return null;
		}
		
		if(interceptor != null)
			// TODO: serializing the OM model here slows the app !!!
			interceptor.doHTTPInvocation(endpoint, body.toString(), this);
		
		SOAPResponse response = backend.invoke(body);

		if(interceptor != null)
			// TODO: serializing the OM model here slows the app !!!
			interceptor.receiveHTTPResult(endpoint, body.toString(), this,
					response.getBody().toString());
		
		// convert the response message back to Daios format
		if(interceptor != null)
			// TODO: serializing the OM model here slows the app !!!
			interceptor.beginOutputConversion(message, outputMsgToUse,
					wsdl, this, response.getBody().toString());		
		DaiosOutputMessage output = null;
		output = SOAPConverter.getInstance(
						wsdl.getBindingStyle().getInvocationStyle(),
						outputMsgToUse, wsdl.getTypes())
					.convert(response.getBody(), response.getMultirefs());
		if(interceptor != null)
			// TODO: serializing the OM model here 
			interceptor.finishedOutputConversion(message, outputMsgToUse,
					wsdl, this, response.getBody().toString(), output);			
		
		// save HTTP and SOAP headers for later retrieval
		responseHttpHeaders = response.getHTTPHeaders();
		responseHeaders = response.getSOAPHeaders();
		
		// return
		return output;
		
	}

	/**
	 * 
	 * Do an asynchronous 'fire and forget'-style call.
	 * The client fires the input message, and does not
	 * expect any kind of answer or confirmation from the
	 * service.
	 * 
	 * @param message The Daios style input message.
	 * 
	 * @throws AmbigiousException
	 * @throws InvocationException
	 */
	public void fireAndForget(DaiosInputMessage message)
		throws AmbigiousException, InvocationException {
		
		log.info("Doing native fire and forget call with message:\n"
				+message.toString());
		
		if(interceptor != null)
			interceptor.beginInvocation("SOAP", "fireandforget",
				message, this);
		
		fireAndForgetInvocation(message);
		
		if(interceptor != null)
			interceptor.finishedInvocation("SOAP", "fireandforget",
				message, this, null);		
	}

	/**
	 * 
	 * Do an asynchronous 'fire and forget'-style call.
	 * The client fires the input message, and does not
	 * expect any kind of answer or confirmation from the
	 * service.
	 * 
	 * @param message The Daios style input message.
	 * 
	 * @throws AmbigiousException
	 * @throws InvocationException
	 */	
	private void fireAndForgetInvocation(DaiosInputMessage message)
		throws InvocationException, AmbigiousException {
		
		// prepare our Daios invocation
		OMElement body = prepareNativeInvocation(message);
		
		// use our native SOAP backend to transfer the message to
		// the endpoint
		SOAPBackend backend = null;
		try {
			backend = new SOAPBackend(new URL(endpoint), log, headers,
					httpHeaders);
		} catch (MalformedURLException e) {
			log.severe("Malformed URL detected.");
			e.printStackTrace();
			return;
		}
		
		if(interceptor != null)
			interceptor.doHTTPInvocation(endpoint, body.toString(), this);
		
		backend.invokeOneway(body);
		
	}

	/**
	 * Do an asynchronous 'callback'-style call.
	 * The client fires the input message, and registers
	 * a <i>callback listener</i> which is notified
	 * when the response is available. The listener
	 * has to implement the interface
	 * <link>at.ac.tuwien.infosys.dsg.daios.framework.interfaces.DaiosCallback</link>
	 * and implement the <i>onComplete</i> and <i>onError</i>
	 * methods accordingly.
	 * 
	 * @param message The Daios-style input message.
	 * @param callback The registered callback listener.
	 * 
	 * @throws AmbigiousException
	 * @throws InvocationException
	 */	
	public void callback(DaiosInputMessage message, IDaiosCallback callback)
		throws AmbigiousException, InvocationException {
	
		if(interceptor != null)
			interceptor.beginInvocation("SOAP", "callback",
				message, this);
		
		callbackInvocation(message, callback);
		
	}

	/**
	 * Do an asynchronous 'callback'-style call.
	 * The client fires the input message, and registers
	 * a <i>callback listener</i> which is notified
	 * when the response is available. The listener
	 * has to implement the interface
	 * <link>at.ac.tuwien.infosys.dsg.daios.framework.interfaces.DaiosCallback</link>
	 * and implement the <i>onComplete</i> and <i>onError</i>
	 * methods accordingly.
	 * 
	 * @param message The Daios-style input message.
	 * @param callback The registered callback listener.
	 * 
	 * @throws AmbigiousException
	 * @throws InvocationException
	 */		
	private void callbackInvocation(DaiosInputMessage message, IDaiosCallback callback) {
		
		// create new thread
		threadFactory.newThread(new AsyncCaller(message, callback))
			.start();
		
	}
	
	/**
	 * Do an asynchronous 'poll object'-style call. The client
	 * fires the input message, and receives a 'poll object'
	 * as response. This poll object can be 'queried' for the
	 * result of the invocation as soon as it is available.
	 * 
	 * <p>
	 * 
	 * The poll object call is an abstraction of the more
	 * general callback call.
	 * 
	 * @param message The Daios-style input message.
	 * @return The poll object which will contain the result
	 * as soon as it is available.
	 * 
	 * @throws AmbigiousException
	 * @throws InvocationException
	 */	
	public PollObject pollObjectCall(DaiosInputMessage message)
		throws AmbigiousException, InvocationException {
	
		if(interceptor != null)
			interceptor.beginInvocation("SOAP", "pollobject",
				message, this);
		
		return pollObjectInvocation(message);
		
	}

	/**
	 * Do an asynchronous 'poll object'-style call. The client
	 * fires the input message, and receives a 'poll object'
	 * as response. This poll object can be 'queried' for the
	 * result of the invocation as soon as it is available.
	 * 
	 * <p>
	 * 
	 * The poll object call is an abstraction of the more
	 * general callback call.
	 * 
	 * @param message The Daios-style input message.
	 * @return The poll object which will contain the result
	 * as soon as it is available.
	 * 
	 * @throws AmbigiousException
	 * @throws InvocationException
	 */	
	private PollObject pollObjectInvocation(DaiosInputMessage message)
		throws AmbigiousException, InvocationException {
		NativePollObject po = new NativePollObject();
		
		NativeCallbackHandler callback = new
			NativeCallbackHandler(po);
		
		callbackInvocation(message, callback);
		
		return po;
	}	
	
	/**
	 * Set the endpoint address of the service. It is not
	 * mandatory to set the endpoint using this method -
	 * if it is not set the endpoint will be read from
	 * the WSDL definition. If the defintion contains more
	 * than one endpoint address usage of this method <i>is</i>
	 * mandatory.
	 * 
	 * @param endpoint The address of the endpoint.
	 */	
	public void setEndpointAddress(URL endpoint) {
		this.endpoint = endpoint.toExternalForm();
	}
	
	/**
	 * Hard-set the name of the WSDL operation to use.
	 * 
	 * @param operation The QName of the operation to use.
	 * 
	 * @throws AmbigiousException 
	 */
	public void setOperation(QName operation) throws AmbigiousException {
		this.fixedOperation = wsdl.getOperation(operation);
		if(this.fixedOperation == null)
			throw new AmbigiousException("Operation "+operation.getLocalPart()+" not found.");		
	}
	
	/**
	 * Hard-set the name of the input message to use.
	 * 
	 * @param message The QName of the input message to use.
	 * 
	 * @throws AmbigiousException
	 */	
	public void setInputMessage(QName message) throws AmbigiousException {
		this.fixedMessage = wsdl.getMessage(message);
		if(this.fixedMessage == null)
			throw new AmbigiousException("Message "+message.getLocalPart()+" not found.");
	}

	/**
	 * Add a certain SOAP header to the invocation.
	 * 
	 * @param header The QName of the header to set.
	 * @param value The String value of the header.
	 * @return For simplicity this method returns the invoker.
	 */
	public NativeServiceInvoker addHeader(QName header, String value) {
		headers.put(header, value);
		return this;
	}

	/**
	 * Remove a certain SOAP header.
	 * 
	 * @param header The QName of the header to remove.
	 * @return For simplicity this method returns the invoker.
	 */	
	public NativeServiceInvoker removeHeader(QName header) {
		headers.remove(header);
		return this;
	}
	
	/**
	 * Get a list of all current header QNames.
	 * 
	 * @return A list of all header QNames.
	 */
	public Enumeration<QName> getHeaders() {
		return headers.keys();
	}
	
	/**
	 * Add a certain HTTP header to the invocation.
	 * 
	 * @param header The String name of the header to set.
	 * @param value The String value of the header.
	 * @return For simplicity this method returns the invoker.
	 */	
	public NativeServiceInvoker addHTTPHeader(String header, String value) {
		httpHeaders.put(header, value);
		return this;
	}

	/**
	 * Remove a certain HTTP header.
	 * 
	 * @param header The String name of the header to remove.
	 * @return For simplicity this method returns the invoker.
	 */		
	public NativeServiceInvoker removeHTTPHeader(String header) {
		httpHeaders.remove(header);
		return this;
	}

	/**
	 * Get a list of all current header names.
	 * 
	 * @return A list of all header names.
	 */	
	public Enumeration<String> getHTTPHeaders() {
		return httpHeaders.keys();
	}	
	
	/**
	 * Get a table containg all response SOAP headers. This
	 * method can be used only <i>after</i> the invocation
	 * has been fired. If it is used before the first invocation
	 * it will return null.
	 * 
	 * @return A table containing all SOAP response headers.
	 */
	public Hashtable<QName, String> getResponseHeaders() {
		return responseHeaders;
	}

	/**
	 * Get a table containg all response HTTP headers. This
	 * method can be used only <i>after</i> the invocation
	 * has been fired. If it is used before the first invocation
	 * it will return null.
	 * 
	 * @return A table containing all HTTP response headers.
	 */
	public Hashtable<String, String> getResponseHTTPHeaders() {
		return responseHttpHeaders;
	}
	
	/**
	 * Hook an interceptor into this frontend.
	 * 
	 * @param interceptor
	 */	
	public void setInterceptor(DaiosInterceptor interceptor) {
		this.interceptor = interceptor;
	}
	
	/**
	 * Prepare an native SOAP invocation. Construct an OMElement that
	 * can be fired using the native SOAP backend.
	 * 
	 * <p>
	 * 
	 * This method carries out a couple of steps:
	 * 
	 * <pre>
	 *   -- first of all, an endpoint is figured out.
	 *   -- after that the input message is compared
	 *      with the messages in the WSDL file, and
	 *      a matching WSDL message is chosen.
	 *   -- then an operation is chosen that has this
	 *      WSDL message as input message.
	 *   -- and finally operation and message are
	 *      converted to AXIOM format and an
	 *      OMElement is created and returned.
	 * </pre>
	 * 
	 * @param message The Daios style input message.
	 * @return The OMElement body.
	 * 
	 * @throws AmbigiousException
	 */
	private OMElement prepareNativeInvocation(DaiosInputMessage message)
		throws AmbigiousException {
		
		// check if we have a endpoint set, otherwise
		// figure it out
		if(endpoint == null)
			endpoint = matcher.getEndpoint();
		
		log.info("Using endpoint: "+endpoint);
		
		IWSDLMessage msgToUse = null;
		// if we do not know which input message to use
		// figure out which input message in the file fits best
		// to our message
		if(fixedMessage == null) {
			
			// if we also have no operation we figur<e out these details from WSDL
			if(fixedOperation == null)
				msgToUse = matcher.getSimilarMsg(message);
			// otherwise we use the input message of this operation
			else {
				msgToUse = wsdl.getMessage(new QName(null,fixedOperation.getInput()));
				matcher.checkCompliance(msgToUse,message);
			}
			
			if(msgToUse == null)
				throw new AmbigiousException("The service does not accept a message with "+
					"such a signature.");
		}
		// otherwise we check the hardset message for compliance
		// with the given input
		else {
			msgToUse = fixedMessage;
			matcher.checkCompliance(msgToUse, message);
		}
			
		log.info("Using message: "+msgToUse.getMessageName());
		
		IWSDLOperation opToUse = null;
		// if we do not know which operation to use
		// figure out which method takes this input message
		if(fixedOperation == null) {
			opToUse = matcher.getOperation(msgToUse);
	
			if(opToUse == null)
				throw new AmbigiousException("The service currently does not support" +
					"such a message.");		
		}
		// otherwise just use this operation
		else {
			opToUse = fixedOperation;
			// check if this op and message fit together
			if(!opToUse.getInput().equals(msgToUse.getMessageName()))
				throw new AmbigiousException("Operation "+opToUse.getName()+
					" does not accept message "+msgToUse.getMessageName()+
					" as input message.");						
		}
		
		log.info("Using operation: "+opToUse.getName());
		
		// now figure out the output message of this operation
		String outname = opToUse.getOutput();
		for(IWSDLMessage msg : wsdl.getMessages()) {
			if(msg.getMessageName().equals(outname)) {
				outputMsgToUse = msg;
				break;
			}
				
		}
		
		log.info("Expected output message: "+outputMsgToUse.getMessageName());		
		
		// convert the input message to XML
		if(interceptor != null)
			interceptor.beginInputConversion(message, opToUse, msgToUse, wsdl, this);
		OMElementConverter converter =
			OMElementConverter.getInstance(
				wsdl.getBindingStyle(),
				outputMsgToUse, wsdl.getTypes());
		OMElement bodyAsOM =
			converter.convert(opToUse, msgToUse, message, wsdl.getTypes());
		if(interceptor != null)
			interceptor.finishedInputConversion(message, opToUse,
					msgToUse, wsdl, this, bodyAsOM.toString());		
		
		return bodyAsOM;
		
	}
	
	/**
	 * Private thread used to fire asynchronous requests.
	 * 
	 * @author philipp
	 *
	 */
	class AsyncCaller implements Runnable {
		
		private IDaiosCallback callback = null;
		private DaiosInputMessage input = null;
		
		/**
		 * Construct a new thread.
		 * 
		 * @param input The input message to use.
		 * @param callback The callback interface that should
		 * be notified when a response comes in.
		 */
		AsyncCaller(DaiosInputMessage input, IDaiosCallback callback) {
			this.callback = callback;
			this.input = input;
		}
		
		/**
		 * Run the thread.
		 */
		public void run() {
			try {
				// we can do a simple 'request/response'
				// invocation here in our separated thread
				DaiosOutputMessage output = 
					requestResponseInvocation(input);
				if(interceptor != null)
					interceptor.finishedInvocation("SOAP", "callback",
						input, NativeServiceInvoker.this, output);
				// notify the callback listener
				callback.onComplete(output);
			} catch (Exception e) {
				log.severe("There was an error while executing your request.");
				e.printStackTrace();
				callback.onError(e);
			} 
		}
		
	}
}
